<?php
/**
 * OpenEMR <http://open-emr.org>.
 *
 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
 */

namespace OpenEMR\Core;

use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Yaml\Exception\ParseException;

/**
 * Class Header.
 *
 * Helper class to generate some `<script>` and `<link>` elements based on a
 * configuration file. This file would be a good place to include other helpers
 * for creating a `<head>` element, but for now it sufficently handles the
 * `setupHeader()`
 *
 * @package OpenEMR
 * @subpackage Core
 * @author Robert Down <robertdown@live.com>
 * @copyright Copyright (c) 2017 Robert Down
 */
class Header
{

    /**
     * Setup various <head> elements.
     *
     * See root_dir/config/config.yaml for available assets
     *
     * Example usage in a PHP view script:
     * ```php
     * // Top of script with require_once statements
     * use OpenEMR\Core\Header;
     *
     * // Inside of <head>
     * // If no special assets are needed:
     * Header::setupHeader();
     *
     * // If 1 special asset is needed:
     * Header::setupHeader('key-of-asset');
     *
     * // If 2 or more assets are needed:
     * Header::setupHeader(['array', 'of', 'keys']);
     * ```
     *
     * Inside of a twig template (Parameters same as before):
     * ```html
     * {{ includeAsset() }}
     * ```
     *
     * Inside of a smarty template, use | (pipe) delimited string of key names
     * ```php
     * {headerTemplate}
     * {headerTemplate assets='key-of-asset'}  (1 optional assets)
     * {headerTemplate assets='array|of|keys'}  (multiple optional assets. ie. via | delimiter)
     * ```
     *
     * The above example will render `<script>` tags and `<link>` tag which
     * bring in the requested assets from config.yaml
     *
     * @param array|string $assets Asset(s) to include
     * @throws ParseException If unable to parse the config file
     * @return string
     */
    public static function setupHeader($assets = [])
    {
        try {
            html_header_show();
            echo self::includeAsset($assets);
        } catch (\InvalidArgumentException $e) {
            error_log($e->getMessage());
        }
    }

    /**
     * Include an asset from a config file.
     *
     * Static function to read in a YAML file into an array, check if the
     * $assets keys are in the config file, and from the config file generate
     * the HTML for a `<script>` or `<link>` tag.
     *
     * This is a private function, use Header::setupHeader() instead
     *
     * @param array|string $assets Asset(s) to include
     * @throws ParseException If unable to parse the config file
     * @return string
     */
    private static function includeAsset($assets = [])
    {

        if (is_string($assets)) {
            $assets = [$assets];
        }

        // @TODO Hard coded the path to the config file, not good RD 2017-05-27
        $map = self::readConfigFile("{$GLOBALS['fileroot']}/config/config.yaml");
        $scripts = [];
        $links = [];

        foreach ($map as $k => $opts) {
            $autoload = (isset($opts['autoload'])) ? $opts['autoload'] : false;
            $alreadyBuilt = (isset($opts['alreadyBuilt'])) ? $opts['alreadyBuilt'] : false;
            $rtl = (isset($opts['rtl'])) ? $opts['rtl'] : false;
            if ($autoload === true || in_array($k, $assets)) {
                $tmp = self::buildAsset($opts, $alreadyBuilt);

                foreach ($tmp['scripts'] as $s) {
                    $scripts[] = $s;
                }

                foreach ($tmp['links'] as $l) {
                    $links[] = $l;
                }

                if ($rtl && $_SESSION['language_direction'] == 'rtl') {
                    $tmpRtl = self::buildAsset($rtl, $alreadyBuilt);
                    foreach ($tmpRtl['scripts'] as $s) {
                        $scripts[] = $s;
                    }

                    foreach ($tmpRtl['links'] as $l) {
                        $links[] = $l;
                    }
                }
            }
        }

        $linksStr = implode("", $links);
        $scriptsStr = implode("", $scripts);
        return "\n{$linksStr}\n{$scriptsStr}\n";
    }

    /**
     * Build an html element from config options.
     *
     * @var array $opts Options
     * @var boolean $alreadyBuilt - This means the path with cache busting segment has already been built
     * @return array Array with `scripts` and `links` keys which contain arrays of elements
     */
    private static function buildAsset($opts = array(), $alreadyBuilt = false)
    {
        $script = (isset($opts['script'])) ? $opts['script'] : false;
        $link = (isset($opts['link'])) ? $opts['link'] : false;
        $basePath = self::parsePlaceholders($opts['basePath']);

        $scripts = [];
        $links = [];

        if ($script) {
            $script = self::parsePlaceholders($script);
            if ($alreadyBuilt) {
                $path = $script;
            } else {
                $path = self::createFullPath($basePath, $script);
            }
            $scripts[] = self::createElement($path, 'script');
        }

        if ($link) {
            if (!is_string($link) && !is_array($link)) {
                throw new \InvalidArgumentException("Link must be of type string or array");
            }

            if (is_string($link)) {
                $link = [$link];
            }

            foreach ($link as $l) {
                $l = self::parsePlaceholders($l);
                if ($alreadyBuilt) {
                    $path = $l;
                } else {
                    $path = self::createFullPath($basePath, $l);
                }
                $links[] = self::createElement($path, 'link');
            }
        }

        return ['scripts' => $scripts, 'links' => $links];
    }

    /**
     * Parse a string for $GLOBAL key placeholders %key-name%.
     *
     * Perform a regex match all in the given subject for anything warpped in
     * percent signs `%some-key%` and if that string exists in the $GLOBALS
     * array, will replace the occurence with the value of that key.
     *
     * @param string $subject String containing placeholders (%key-name%)
     * @return string The new string with properly replaced keys
     */
    public static function parsePlaceholders($subject)
    {
        $re = '/%(.*)%/';
        $matches = [];
        preg_match_all($re, $subject, $matches, PREG_SET_ORDER, 0);

        foreach ($matches as $match) {
            if (array_key_exists($match[1], $GLOBALS)) {
                $subject = str_replace($match[0], $GLOBALS["{$match[1]}"], $subject);
            }
        }

        return $subject;
    }

    /**
     * Create the actual HTML element.
     *
     * @param string $path File path to load
     * @param string $type Must be `script` or `link`
     * @return string mixed HTML element
     */
    private static function createElement($path, $type)
    {

        $script = "<script type=\"text/javascript\" src=\"%path%\"></script>\n";
        $link = "<link rel=\"stylesheet\" href=\"%path%\" type=\"text/css\">\n";

        $template = ($type == 'script') ? $script : $link;
        $v = $GLOBALS['v_js_includes'];
        $path = $path . "?v={$v}";
        return str_replace("%path%", $path, $template);
    }

    /**
     * Create a full path from given parts.
     *
     * @param string $base Base path
     * @param string $path specific path / filename
     * @return string The full path
     */
    private static function createFullPath($base, $path)
    {
        return $base . $path;
    }

    /**
     * Read a config file and turn it into an array.
     *
     * @param string $file Full path to filename
     * @return array Array of assets
     */
    private static function readConfigFile($file)
    {
        try {
            $config = Yaml::parse(file_get_contents($file));
            return $config['assets'];
        } catch (ParseException $e) {
            error_log($e->getMessage());
            // @TODO need to handle this better. RD 2017-05-24
        }
    }
}
